/*
    Copyright 2009 Barnet Wagman

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
 */

package ot.s3;

import java.io.*;
import org.jets3t.service.*;
import org.jets3t.service.security.*;
import org.jets3t.service.impl.rest.httpclient.*;
import org.jets3t.service.model.*;
import java.util.*;
import ot.*;

/** The class performs the actual S3 operations, using jets3t's REST
 *  service class. All other classes use
 *  the methods in <tt>OpsS3</tt> to access S3.<p>
 * 
 *  The methods in this class are 'one-shot' operations.  If an operation
 *  fails, an exception is thrown immediately.  Methods in this class
 *  do <i>not</i> retry. Retries are implemented in {@link ManagedOps}.
 *  Note, however that jets3t itself performs
 *  retries, as does HttpClient, which jets3t uses.
 *  For jets3t operations, the
 *  default number of retries is five. It can be reset using
 *  {@link #setJets3tProperty(java.lang.String, java.lang.String) }.
 *  <p>
 * 
 *  The S3 ops read and write byte arrays.  Blocking, serialization, and 
 *  (optional) encryption is handled at a higher level (see {@link Serializer}.
 *  In this class,
 *  S3 objects are identified by actual S3 keys, not by virtual pathnames.<p>
 * 
 *  Methods in this class do not return until their operations are complete
 *  (or fail).  
 *
 * @author Barnet Wagman
 */
public class OpsS3 {

    private AWSCredentials acs;
    S3Service s3Read, s3Write;
    
    Jets3tProperties jp;
            
    public OpsS3(String awsAccessID, String awsSecretKey) 
        throws S3ServiceException {
                      
//        System.setProperty("org.apache.commons.logging.Log",
//                           "ot.s3.NilLog");
        
        acs = new AWSCredentials(awsAccessID,awsSecretKey);      
        jp = createDefaultJets3tProperties();
        
        s3Read  = new RestS3Service(acs, null,null,jp);                           
        s3Write = new RestS3Service(acs, null,null,jp);
    }

    public AWSCredentials getCredentials() { return(acs); }
    
        /** Creates a completely new instance of <tt>OpsS3</tt> using
         *  the access key and secret key that were used to create
         *  this instance.
         * 
         * @return
         */
    public OpsS3 cloneOpsS3() throws S3ServiceException {        
        return( new OpsS3(acs.getAccessKey(),acs.getSecretKey()) );
    }
    
    void replaceReadService() throws S3ServiceException {
        s3Read = new RestS3Service(acs, null,null,jp); 
    }
    
    void replaceWriteService() throws S3ServiceException {
        s3Write = new RestS3Service(acs, null,null,jp); 
    }
     
        /** Sets a JetS3t property.
         *  <p>
         *  To ensure that the new property value is in effect, this method
         *  replaces the current <tt>S3Service</tt> objects.
         * @param name
         * @param value
         */
    public void setJets3tProperty(String name, String value)
        throws S3ServiceException {
        
        jp = createDefaultJets3tProperties();
        jp.setProperty(name, value);
        s3Read = new RestS3Service(acs,null,null,jp);
        s3Write = new RestS3Service(acs,null,null,jp);
    }

    public boolean s3ObjectExists(String bucketName,String key) 
        throws S3ServiceException {
        
        try {        
            S3Object o = s3Read.getObjectDetails(new S3Bucket(bucketName), key);               
            return(true);
        }
        catch(S3ServiceException sx) { 
            if ( sx.getResponseCode() == 404 ) return(false);
            else throw sx;
        }        
    }
    
    public void putS3ObjBytes(String bucketName,String key,byte[] b)
        throws S3ServiceException, IOException {        
       
        S3Object o = null;
        try {
            o = new S3Object(key);
            o.setContentLength(b.length);
            o.setDataInputStream(new ByteArrayInputStream(b));
            s3Write.putObject(bucketName,o);
        }
        finally {            
            if ( o != null ) { o.closeDataInputStream(); o = null; }
        }
    }
    
    public byte[] getS3ObjBytes(String bucketName,String key)
        throws IOException, S3ServiceException {                        
        return(getBytes(s3Read.getObject(new S3Bucket(bucketName), key)));                
    }
    
    public void deleteS3Object(String bucketName,String key)
        throws S3ServiceException {        
        
        s3Write.deleteObject(bucketName, key);
    }
    
        /** Gets the keys to all S3 objects in the specified bucket.
         * 
         * @return
         */
    public List<String> listS3Objects(String bucketName) throws S3ServiceException {
               
        S3Object[] objs = s3Read.listObjects(new S3Bucket(bucketName));

        List<String> keys = new ArrayList<String>();

        for ( S3Object s3o : objs ) {
            keys.add(s3o.getKey());
        }

        return(keys);       
    }

        /** Gets the keys to all S3 objects in the specified bucket
         *  whose paths (s3 key) starts with <tt>prefix</tt>.
         *
         * @return
         */
    public List<String> listS3Objects(String bucketName, String prefix)
        throws S3ServiceException {

        S3Object[] objs =
            s3Read.listObjects(new S3Bucket(bucketName),prefix,null);

        List<String> keys = new ArrayList<String>();

        for ( S3Object s3o : objs ) {
            keys.add(s3o.getKey());
        }

        return(keys);
    }

    public List<String> listBuckets() throws S3ServiceException {
               
        S3Bucket[] objs = s3Read.listAllBuckets();

        List<String> bucketNames = new ArrayList<String>();

        for ( S3Bucket b : objs ) {
            bucketNames.add(b.getName());
        }

        return(bucketNames);       
    }    
    
        /** @return true if successful, false if the bucket already exists.
         * 
         * @param bucketName
         * @return
         * @throws org.jets3t.service.S3ServiceException
         */
    public boolean createBucket(String bucketName) throws S3ServiceException {
        try {
            if ( bucketExists(bucketName) ) return(false);
            s3Write.createBucket(bucketName);
            return(true);
        }
        catch(S3ServiceException sx) {
            String errCode = sx.getS3ErrorCode();
            if ( (errCode != null) &&
                 errCode.equals(ConstantsOtS3.S3_ERROR_BUCKET_ALREADY_EXISTS) ) {
                return(false);
            }
            else throw sx;
        }        
    }
    
         /** @return true if successful, false if the bucket does not exists.
         * 
         * @param bucketName
         * @return
         * @throws org.jets3t.service.S3ServiceException
         */
    public boolean deleteBucket(String bucketName) throws S3ServiceException {
        try {
            if ( !bucketExists(bucketName) ) return(false);           
            s3Write.deleteBucket(bucketName);
            return(true);
        }
        catch(S3ServiceException sx) {
            String errCode = sx.getS3ErrorCode();
            if ( (errCode != null) &&
                 errCode.equals(ConstantsOtS3.S3_ERROR_BUCKET_ALREADY_EXISTS) ) {
                return(false);
            }
            else throw sx;
        }        
    }
    
        /** <i>Warning</i>: S3 does not allow us to determine if a bucket 
         *  name is already used.  This method performs a 'bucket accessible'  
         *  test, which returns <tt>true</tt> if the bucket exists and we
         *  have permission to access it.  It returns <tt>false</tt> either if
         *  the bucket name does not exist or if we do not have permission
         *  so access it.  To determine if a bucket names is available, we
         *  must use <tt>createBucket()</tt>.
         * @param bucketName
         * @return
         * @throws org.jets3t.service.S3ServiceException
         */
    public boolean bucketExists(String bucketName) throws S3ServiceException {
        return( s3Read.isBucketAccessible(bucketName) );
    }
    
        // -------------------------------------------------------
    
    byte[] getBytes(S3Object o3) throws IOException, S3ServiceException {
                
        BufferedInputStream ins = null;
        try {
            ins = new BufferedInputStream(o3.getDataInputStream());        
            return( readBytes(ins,(int) o3.getContentLength()) );                           
        }
        
        finally {
            if ( o3 != null ) { o3.closeDataInputStream(); o3 = null; }
            if ( ins != null ) { ins.close(); ins = null; }
        }
    }
    
    private byte[] readBytes(BufferedInputStream ins, int len)  
        throws IOException {
        
        byte[] bytes = new byte [len];
        int nRead = 0;
        while ( nRead < len ) {
            int n = ins.read(bytes, nRead, len - nRead);
            if ( n > 0 ) nRead += n;                    
        }
        return(bytes);
    }      
    
    int getMaxNThreads() {
        return( jp.getIntProperty("httpclient.max-connections",10) );
    }
    
        /** This methods sets values for all properties in the default
         *  property file in the jet3set package except for gui related
         *  properties.<p>
         * 
         * 
         *  TMP: javadoc should list ALL values set.
         *          
         * 
         * @return
         */
    public Jets3tProperties createDefaultJets3tProperties() {
        
        Jets3tProperties jp = new Jets3tProperties();

        // ###
        // # Upload properties for applications.
        // ###
        jp.setProperty("uploads.storeEmptyDirectories","true");

        // ###
        // # Download properties for applications.
        // ###
        jp.setProperty("downloads.restoreLastModifiedDate","false");

        // ###
        // # S3Service properties
        // ###
        jp.setProperty("s3service.https-only","false"); // true
        jp.setProperty("s3service.max-thread-count","8"); // 4
        jp.setProperty("s3service.admin-max-thread-count","10");
        jp.setProperty("s3service.stream-retry-buffer-size","524288"); // "131072");
        jp.setProperty("s3service.internal-error-retry-max","5");
        jp.setProperty("s3service.default-bucket-location","US");
//        jp.setProperty("s3service.s3-endpoint","INCORRECT"); // "s3.amazonaws.com"); // Set to "incorrect" for test.
        jp.setProperty("s3service.disable-dns-buckets","false");

        // #s3service.ignore-exceptions-in-multi=false

        // ###
        // # REST/HTTP HttpClient Implementation properties
        // ###
        jp.setProperty("httpclient.connection-timeout-ms","60000");
        jp.setProperty("httpclient.socket-timeout-ms","60000");
        jp.setProperty("httpclient.max-connections", "10" );// "10"); 
        jp.setProperty("httpclient.stale-checking-enabled","true");
        jp.setProperty("httpclient.retry-max","5");
        // #httpclient.useragent=My User Agent String
        // #httpclient.read-throttle=50
        jp.setProperty("httpclient.authentication-preemptive","false");
        jp.setProperty("httpclient.proxy-autodetect","true");
        // #httpclient.proxy-host=
        // #httpclient.proxy-port=

        // # TCP window size hints for kernel 
        // #httpclient.socket-receive-buffer=65536
        // #httpclient.socket-send-buffer=65536

        // ###
        // # File/Object comparison properties
        // ###
        // #filecomparer.use-md5-files=false
        // #filecomparer.generate-md5-files=false
        // #filecomparer.skip-upload-of-md5-files=false
        // #filecomparer.ignore-panic-dir-placeholders=false
        // #filecomparer.assume-local-latest-in-mismatch=false

        // ###
        // # XML Parsing properties
        // ###
        jp.setProperty("xmlparser.sanitize-listings","true");
       
        // ###
        // # Encryption Algorithm
        // #
        // # Note that some cryptography options will only be available
        // # if the Java Cryptography Extension (JCE) Unlimited Strength 
        // # Jurisdiction Policy Files are installed.
        // ###
        jp.setProperty("crypto.algorithm","PBEWithMD5AndDES");
       
        return(jp);
    }    
}
